require 'rubygems'
require 'xml/libxml'
require 'rexml/document'
require 'family_tree_api/person/information'
require 'family_tree_api/person/summary'
require 'family_tree_api/person/assertions/base'
require 'family_tree_api/person/assertions/name'
require 'family_tree_api/person/assertions/gender'
require 'family_tree_api/person/assertions/event'
require 'family_tree_api/person/assertions/fact'
require 'family_tree_api/person/assertions/relationship'
require 'family_tree_api/person/assertions/ordinance'

module FamilyTreeApi
  module Person
    NS = ['fs:http://api.familysearch.org/familytree/v1','fsapi:http://api.familysearch.org/v1']
    # Builds an xml request for combining individuals.
    # 
    # ==Params
    # * hash_array: should be an array of hashes containing a :ref
    #   and a :version of the individuals to be combined
    def self.combine_request(hash_array)
      validate_combine_parameters(hash_array)
      doc = skeleton_xml
      root = doc.root
      c_persons = root.add_element("persons").
                    add_element("person", {'tempId' => 'JIMMYZ'}).
                    add_element("composition").
                    add_element("persons")
      hash_array.each do |h|
        c_person = c_persons.add_element("person", {'version' => h[:version], 'ref' => h[:ref]})
      end
      doc.to_s
    end
    
    private
    def self.validate_combine_parameters(hash_array)
      unless  hash_array.is_a?(Array) && 
              hash_array.select{|h|
                h.is_a?(Hash) && !h[:ref].nil? && !h[:version].nil?
              }.size == hash_array.size && 
              hash_array.size >= 2
        raise "Invalid Parameters" 
      end
    end
    
    def self.skeleton_xml
      doc = REXML::Document.new
      doc << REXML::XMLDecl.new('1.0', 'utf-8')
      doc.add_element("familytree", {'xmlns:fsapi-v1' => 'http://api.familysearch.org/v1' , 'xmlns' => 'http://api.familysearch.org/familytree/v1'})
      doc
    end
    
    class Base
      attr_accessor :version, :modified, :id, :information, :names, :genders, :events, :facts, :relationships, :father, :mother, :children, :spouses, :summary
      
      def initialize
        @names = []
        @genders = []
        @events = []
        @facts = []
        @relationships = []
        @children = []
        @spouses = []
        @summary = Summary.new
      end
      
      def assertions(state = :all)
        case state
        when :all
          @names + @genders + @events + @facts + @relationships
        when :new
          @names.select { |n| n.state == :new } +
          @genders.select { |g| g.state == :new } +
          @events.select { |e| e.state == :new } +
          @facts.select { |f| f.state == :new } +
          @relationships.select { |r| r.state == :new }
        else
          []
        end
      end
      
      # Adds a new name assertion if a name assertion doesn't already exist
      # with the same name
      # ==Params
      # * name_str: is a string representing the name. Can use the format of 
      #   GivenName /Surname/ to specify parts
      # * options: a hash of assertion options
      #   * type: type of name (defaults to 'Name' - see API doc for more options)
      #   * tempId: temporary id to map a new assertion after saving
      def add_name(name_str, options = 'Name')
        # check to see if it is a string first to maintain backwards compatibility of being a string previously
        if options.is_a? String
          type = options
          options = {}
          options[:type] = type
        else
          options[:type] ||= 'Name'
        end
        if has_surname(name_str)
          add_full_name(name_str, options)
        elsif options[:force] || @names.select { |n| n.type == options[:type] && n.full_text == name_str }.size == 0
          name = FamilyTreeApi::Person::Assertion::Name.new
          name.type = options[:type]
          name.tempId = options[:tempId] if options[:tempId]
          name.full_text = name_str
          @names << name
        end
      end
      
      def add_full_name(name_str, options = 'Name')
        if options.is_a? String
          type = options
          options = {}
          options[:type] = type
        else
          options[:type] ||= 'Name'
        end
        match_result = name_str.match(/(.*)\/(.*)\//)
        given_names = match_result[1]
        family_names = match_result[2]
        full_text_name = given_names + family_names
        if options[:force] || @names.select { |n| n.type == options[:type] && n.full_text == full_text_name }.size == 0
          name = FamilyTreeApi::Person::Assertion::Name.new
          name.type = options[:type]
          name.tempId = options[:tempId] if options[:tempId]
          name.set_pieces :given => given_names, :family => family_names
          @names << name
        end
      end
      
      def full_names(type = 'Name')
        @names.select{|n| n.type==type}.collect { |n| n.forms.first.full_text }
      end
      
      def add_parent(parent_ref, options = 'Biological')
        if options.is_a? String
          detail = options
          options = {}
          options[:detail] = detail
        else
          options[:detail] ||= 'Biological'
        end
        if options[:force] || @facts.select { |f| f.detail == options[:detail] && f.type == 'Lineage' && f.scope == 'parent-child' && f.parent && f.parent.ref == parent_ref }.size == 0
          @facts << Assertion::Fact.new_parent(parent_ref, options)
        end
      end
      
      def parent_refs
        self.parent_assertions.collect { |f| f.parent.ref  }
      end
      
      def parent_assertions
        @facts.select { |f| f.type=='Lineage' && f.scope=='parent-child' && !f.parent.nil? }
      end
      
      def add_child(child_ref, options = 'Biological')
        if options.is_a? String
          detail = options
          options = {}
          options[:detail] = detail
        else
          options[:detail] ||= 'Biological'
        end
        if options[:force] || @facts.select { |f| f.detail == options[:detail] && f.type == 'Lineage' && f.scope == 'parent-child' && f.child && f.child.ref == child_ref }.size == 0
          @facts << Assertion::Fact.new_child(child_ref, options)
        end
      end
      
      def child_refs
        self.child_assertions.collect { |f| f.child.ref }
      end
      
      def child_assertions
        @facts.select { |f| f.type=='Lineage' && f.scope =='parent-child' && !f.child.nil? }
      end
      
      def add_spouse(spouse_ref, options = {})
        if options[:force] || @relationships.select { |r| r.scope == 'couple' && r.spouse && r.spouse.ref == spouse_ref }.size == 0
          @relationships << Assertion::Relationship.new_spouse(spouse_ref, gender, options)
        end
      end
      
      def spouse_refs
        self.spouse_assertions.collect { |r| r.spouse.ref }
      end
      
      def spouse_assertions
        @relationships.select { |r| r.scope == 'couple' && !r.spouse.nil? }
      end
      
      def add_gender(gender_val, options = {})
        if options[:force] == true || @genders.select { |g| g.value == gender_val }.size == 0
          @genders << Assertion::Gender.new(gender_val, options)
        end
      end
      
      def gender
        @genders.size > 0 ? @genders.first.value : ''
      end
      
      
      def add_marriage(options = {})
        e = Assertion::Event.new(:type => 'Marriage')
        e.date = options[:date] if options[:date]
        e.place = options[:place] if options[:place]
        e.add_note options[:notes] if options[:notes]
        e.tempId = options[:tempId] if options[:tempId]
        e.scope = 'couple'
        spouse_role = begin
          if gender == 'Male'
            'Woman'
          elsif gender == 'Female'
            'Man'
          else
            'Unknown'
          end
        end
        e.spouse = Assertion::Spouse.new :ref => options[:spouse], :role => spouse_role
        unless !options[:force] && @events.select {|ev| ev == e }.size > 0
          @events << e
        end
      end
      
      def add_divorce(options = {})
        e = Assertion::Event.new(:type => 'Divorce')
        e.date = options[:date] if options[:date]
        e.place = options[:place] if options[:place]
        e.add_note options[:notes] if options[:notes]
        e.scope = 'couple'
        spouse_role = begin
          if gender == 'Male'
            'Woman'
          elsif gender == 'Female'
            'Man'
          else
            'Unknown'
          end
        end
        e.spouse = Assertion::Spouse.new :ref => options[:spouse], :role => spouse_role
        unless !options[:force] && @events.select {|ev| ev == e }.size > 0
          @events << e
        end
      end
      # Adds an event to the person if the event doesn't already exist
      # 
      # === Params
      # * options: is a hash with optional :date and :place which
      #   should be assigned strings of the date or place
      def add_event(type, options = {})
        e = Assertion::Event.new(:type => type.capitalize)
        e.date = options[:date] if options[:date]
        e.place = options[:place] if options[:place]
        e.tempId = options[:tempId] if options[:tempId]
        e.add_note options[:notes] if options[:notes]
        unless !options[:force] && @events.select { |ev| ev == e }.size > 0
          @events << e
        end
      end
      
      def set_summary(summary_hash)
        @summary.set_values(summary_hash)
      end
      
      def get_event(type)
        @events.select{|e| e.type == type.capitalize }.first
      end
      
      def get_events(type)
        @events.select { |e| e.type == type.capitalize }
      end
      
      
      # Method missing handles several types of method calls and delegates them to
      # special methods to handle assignations or getters
      def method_missing(method_id, *options)
        if match = /^add_(adoption|baptism|birth|blessing|burial|christening|death)$/.match(method_id.to_s)
          add_event(match[1],*options)
          # send("assign_#{match[2]}",match[1], *options) # match[1] => birth,death,etc.; match[2] => date,place
        elsif match = /^(adoption|baptism|birth|blessing|burial|christening|death|marriage|divorce)$/.match(method_id.to_s)
          get_event(match[1])
        elsif match = /^(adoption|baptism|birth|blessing|burial|christening|death|marriage|divorce)s$/.match(method_id.to_s)
          get_events(match[1])
        else
          super
        end
      end
      
      def parse(p_element)
        @version = p_element.attributes["version"]
        @modified = p_element.attributes["modified"]
        @id = p_element.attributes["id"]
        p_element.children.each do |e|
          if e.name == 'information' 
              @information = Information.parse e
          elsif e.name == 'assertions'
            e.children.each do |a|
              case a.name
              when 'name' then
                @names << FamilyTreeApi::Person::Assertion::Name.parse(a)
              when 'gender' then
                @genders << FamilyTreeApi::Person::Assertion::Gender.parse(a)
              when 'event' then
                @events << FamilyTreeApi::Person::Assertion::Event.parse(a)
              when 'fact' then
                @facts << FamilyTreeApi::Person::Assertion::Fact.parse(a)
              when 'relationship' then
                @relationships << FamilyTreeApi::Person::Assertion::Relationship.parse(a)
              end
            end
          end
        end
      end
      
      def self.parse(xml_string, ancestors = 0, descendants = 0)
        xp = XML::Parser.new
        xp.string = xml_string
        doc =  xp.parse.root
        people = doc.find("/fs:familytree/fs:persons/fs:person",NS).collect do |p_element|
          person = self.new
          person.parse p_element
          person
        end
        
        link_ancestors people if ancestors > 0
        link_children people if descendants > 0
        link_spouses people if descendants > 0
        # TODO: link spouses for descendants
        person = people.first
        person
      end
      
      # Params
      # * assertion_state: can be :all (default), :new, or :summary
      def to_xml(assertion_state = :all)
        doc = FamilyTreeApi::Person.skeleton_xml
        person = doc.root.add_element("persons").
                add_element("person")
        person.add_attribute('id', @id) if @id
        person.add_attribute('tempId', 'JIMMYZ') if @id.nil?
        person.add_attribute("version", @version) if @version
        person.add_attribute("modified", @modified) if @modified
        if assertion_state == :summary && @summary
          summary = person.add_element(@summary.to_xml)
        else
          asserts = person.add_element("assertions")
          assertions(assertion_state).each do |a| 
            asserts.add_element(a.to_xml)  
          end
        end
        doc.to_s
      end
      
      private
      # checks to see if there is // in the string to parse out a surname
      def has_surname(name)
        name =~ /\/.*\//
      end
      
      def self.link_ancestors(people)
        people_index = create_index people
        # link the people together
        people.each do |person|
          parent_facts = person.facts.select { |f| f.type == "Lineage" && f.scope == 'parent-child' && f.parent }
          parent_facts.each do |f|
            person.father = people_index[f.parent.ref] if people_index[f.parent.ref] && people_index[f.parent.ref].information.gender == 'Male'
            person.mother = people_index[f.parent.ref] if people_index[f.parent.ref] && people_index[f.parent.ref].information.gender == 'Female'
          end
        end
      end

      def self.link_children(people)
        people_index = create_index people
        # link the parents to their children
        people.each do |person|
          children_facts = person.facts.select {|f| f.type == "Lineage" && f.scope == 'parent-child' && f.child }
          relationships = person.relationships.select {|r| r.scope == 'parent-child' && r.child }
          children_facts = children_facts + relationships
          children = children_facts.collect do |f|
            people_index[f.child.ref] if people_index[f.child.ref]
          end
          person.children = children.uniq.select{|c| !c.nil? }
        end
      end
      
      def self.link_spouses(people)
        people_index = create_index people
        # link the parents to their children
        people.each do |person|
          relationships = person.relationships.select {|f| f.scope == 'couple' && f.spouse }
          spouses = relationships.collect do |f|
            people_index[f.spouse.ref] if people_index[f.spouse.ref]
          end
          person.spouses = spouses.uniq.select{|s| !s.nil? }
        end
      end

      # create some kind of index so that we can easily find a person by id
      def self.create_index(people)
        index = Hash.new
        people.each do |person|
          index[person.id] = person
        end
        index
      end
    end    
  end
end